﻿using System;
using System.Collections.Generic;
using MLSharp.Utilities;

namespace MLSharp.SubgroupDiscovery
{
	/// <summary>
	/// Extends <see cref="Rule"/> with additional properties
	/// needed for the SD algorithm.
	/// </summary>
	/// <remarks>
	/// This type is *NOT* currently thread-safe.
	/// </remarks>
	public class SdRule : Rule
	{
		#region Private Fields

		/// <summary>
		/// The conditions of the rule.
		/// </summary>
		private readonly List<RuleCondition> mConditions = new List<RuleCondition>();

		/// <summary>
		/// A table for quickly locating an attribute's index.
		/// </summary>
		private IDictionary<string, int> mAttributeIndex;

		#endregion

		#region Public Overrides

		/// <summary>
		/// Gets the current set of rule conditions as an array.
		/// </summary>
		public override RuleCondition[] Conditions
		{
			get
			{
				return mConditions.ToArray();
			}
			set
			{
				mConditions.Clear();
				mConditions.AddRange(value);
			}
		}

		/// <summary>
		/// Gets the number of instances that are correctly covered by the rule.
		/// </summary>
		public override int Correct
		{
			get
			{
				return TruePositives.Count;
			}
			set
			{
				throw new NotSupportedException("This value is calculated by the True and False positive instances and cannot be set.");
			}
		}

		/// <summary>
		/// Gets the total number of instances that are covered by the rule.
		/// </summary>
		public override int Cover
		{
			get
			{
				return TruePositives.Count + FalsePositives.Count;
			}
			set
			{
				throw new NotSupportedException("This value is calculated by the True and False positive instances and cannot be set.");
			}
		}

		/// <summary>
		/// Gets the weighted relative accuracy for the rule.
		/// </summary>
		public double WeightedRelativeAccuracy
		{
			get
			{
				return Cover/(double)TotalInstanceCount*(Correct/(double)Cover - TotalPositiveCount/(double)TotalInstanceCount);
			}
		}

		#endregion

		#region Internal Properties

		/// <summary>
		/// The total number of positive instances in the data set.
		/// </summary>
		internal int TotalPositiveCount { get; set; }

		/// <summary>
		/// The total number of instances in the data set.
		/// </summary>
		internal int TotalInstanceCount { get; set; }

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets a reference to a mutable list of conditions in the rule.
		/// </summary>
		public List<RuleCondition> ConditionsList
		{
			get
			{
				return mConditions;
			}
		}

		/// <summary>
		/// The quality score of the rule.
		/// </summary>
		public double Quality { get; set; }

		/// <summary>
		/// The indexes of positive instances that are covered by this rule.
		/// </summary>
		public HashSet<int> TruePositives { get; private set; }

		/// <summary>
		/// The indexes of negative instances that are incorrectly covered by this rule.
		/// </summary>
		public HashSet<int> FalsePositives { get; private set; }

		#endregion

		#region Pubilc Constructors

		/// <summary>
		/// Initializes the rule for use.
		/// </summary>
		public SdRule()
		{
			TruePositives = new HashSet<int>();
			FalsePositives = new HashSet<int>();
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Returns true if the rule covers the instance at the specified index,
		/// false otherwise.
		/// </summary>
		/// <param name="dataSet"></param>
		/// <param name="instanceIndex"></param>
		/// <returns></returns>
		public bool CoversInstance(IDataSet dataSet, int instanceIndex)
		{
			Instance instance = dataSet.Instances[instanceIndex];

			//If the attribute index hasn't been initialized, initialize it now.
			if (mAttributeIndex == null)
			{
				mAttributeIndex = new Dictionary<string, int>();

				for (int i=0; i < dataSet.Attributes.Count; i++)
				{
					mAttributeIndex.Add(dataSet.Attributes[i].Name, i);
				}
			}

			foreach (RuleCondition condition in mConditions)
			{
				int attributeIndex = mAttributeIndex[condition.Attribute];

				//If the instance has a null for that value, what do we do?
				if (instance.IsValueMissing(attributeIndex))
				{
					throw new NotSupportedException("Rules cannot be applied to instances with missing values.");
				}

				object value = dataSet.Attributes[attributeIndex].IsNumeric() ?
					(object)instance.Values[attributeIndex] : instance.GetStringValue(attributeIndex);


				switch (condition.OperatorType)
				{
					case ConditionOperatorType.Equals:
						if (value != condition.Value)
						{
							return false;
						}
						break;

					case ConditionOperatorType.NotEqual:
						if (value == condition.Value)
						{
							return false;
						}
						break;

					case ConditionOperatorType.LessThanOrEqual:
					case ConditionOperatorType.GreaterThan:

						//These two condition types are only defined on numeric attributes,
						//so this cast should be safe.
						double conditionValue = (double)condition.Value;
						double instanceValue = (double)value;

						if (condition.OperatorType == ConditionOperatorType.LessThanOrEqual &&
							instanceValue > conditionValue)
						{
							return false;
						}
						else if (condition.OperatorType == ConditionOperatorType.GreaterThan &&
							instanceValue <= conditionValue)
						{
							return false;
						}
						break;
				}
			}

			//If execution reaches this point, everything passed, so the rule covers the instance.
			return true;
		}

		/// <summary>
		/// Creates a deep copy.
		/// </summary>
		/// <returns></returns>
		public SdRule Clone()
		{
			SdRule rule = new SdRule();
			rule.TruePositives.AddRange(TruePositives);
			rule.FalsePositives.AddRange(FalsePositives);
			rule.Class = Class;
			rule.ConditionsList.AddRange(ConditionsList);
			rule.Quality = Quality;
			rule.TotalInstanceCount = TotalInstanceCount;
			rule.TotalPositiveCount = TotalPositiveCount;

			return rule;
		}

		#endregion

		#region Overrides

		/// <summary>
		/// Writes the relevant stats about the SdRule to the string builder.
		/// </summary>
		/// <param name="sb"></param>
		protected override void WriteRuleStats(System.Text.StringBuilder sb)
		{
			sb.AppendFormat("(Class=\"{0}\" Correct=\"{1}\" Cover=\"{2}\" WRA=\"{3}\")",
							Class,
							Correct,
							Cover,
							WeightedRelativeAccuracy);
		}

		#endregion
	}
}
